package cn.cleanarch.infra.idp.pms.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 安全预览令牌工具类
 * 使用nonce + timestamp + sign参数传递方式，通过双重MD5加密验证参数完整性
 */
@Component
public class SecurePreviewTokenUtil {
    
    private static final Logger logger = LoggerFactory.getLogger(SecurePreviewTokenUtil.class);
    
    private static final long DEFAULT_EXPIRE_TIME = 24 * 60 * 60 * 1000L; // 默认24小时
    private static final long NONCE_EXPIRE_TIME = 10 * 60 * 1000L; // nonce保留10分钟，防止重放攻击
    
    @Value("${idp.preview.token.secret:IlwekU03jOU23lCx23l4kjTr234jjoU23ljTr2Cii79234}")
    private String secretKey;
    
    // 存储已使用的nonce，用于防止重放攻击
    private final Set<String> usedNonces = ConcurrentHashMap.newKeySet();
    private final ScheduledExecutorService nonceCleanupExecutor = Executors.newScheduledThreadPool(1);
    
    public SecurePreviewTokenUtil() {
        // 定期清理过期的nonce
        nonceCleanupExecutor.scheduleAtFixedRate(this::cleanupExpiredNonces, 1, 1, TimeUnit.MINUTES);
    }
    
    /**
     * 生成签名参数
     * 
     * @param fileId 文件ID
     * @param expireTime 过期时间（毫秒），null表示使用默认时间
     * @return 签名参数对象
     */
    public SignParams generateSignParams(Long fileId, Long expireTime) {
        if (fileId == null) {
            throw new IllegalArgumentException("文件ID不能为空");
        }
        
        long expireAt = System.currentTimeMillis() + (expireTime != null ? expireTime : DEFAULT_EXPIRE_TIME);
        String nonce = generateNonce();
        
        // 构造待签名字符串：fileId + timestamp + nonce + secretKey
        String signStr = fileId + expireAt + nonce + secretKey;
        
        // 双重MD5加密
        String sign = doubleMd5(signStr);
        
        // 对生成的签名进行URL编码，防止+号等特殊字符被解析
        String encodedSign = java.net.URLEncoder.encode(sign, java.nio.charset.StandardCharsets.UTF_8);
        
        SignParams params = new SignParams();
        params.fileId = fileId;
        params.timestamp = expireAt;
        params.nonce = nonce;
        params.sign = encodedSign;
        
        logger.info("生成签名参数 - 文件ID: {}, 时间戳: {}, nonce: {}, 签名: {}", fileId, new java.util.Date(expireAt), nonce, encodedSign);
        return params;
    }

    /**
     * 为版本化ID生成签名参数（支持文件ID和版本ID）
     * 
     * @param id 文件ID或版本ID
     * @param idType ID类型：file或version
     * @param expireTime 过期时间（毫秒），null表示使用默认时间
     * @return 签名参数对象
     */
    public SignParams generateSignParamsForVersioned(Long id, String idType, Long expireTime) {
        if (id == null || idType == null) {
            throw new IllegalArgumentException("ID和ID类型不能为空");
        }
        
        if (!"file".equals(idType) && !"version".equals(idType)) {
            throw new IllegalArgumentException("ID类型只能是 'file' 或 'version'");
        }
        
        long expireAt = System.currentTimeMillis() + (expireTime != null ? expireTime : DEFAULT_EXPIRE_TIME);
        String nonce = generateNonce();
        
        // 构造待签名字符串：id + idType + timestamp + nonce + secretKey
        String signStr = id + idType + expireAt + nonce + secretKey;
        
        // 双重MD5加密
        String sign = doubleMd5(signStr);
        
        // 对生成的签名进行URL编码，防止+号等特殊字符被解析
        String encodedSign = java.net.URLEncoder.encode(sign, java.nio.charset.StandardCharsets.UTF_8);
        
        SignParams params = new SignParams();
        params.fileId = id; // 使用原始ID
        params.timestamp = expireAt;
        params.nonce = nonce;
        params.sign = encodedSign;
        
        logger.info("生成版本化ID签名参数 - 原始ID: {}, ID类型: {}, 时间戳: {}, nonce: {}, 签名: {}", 
                   id, idType, new java.util.Date(expireAt), nonce, encodedSign);
        return params;
    }
    
    /**
     * 生成默认有效期的签名参数
     * 
     * @param fileId 文件ID
     * @return 签名参数对象
     */
    public SignParams generateSignParams(Long fileId) {
        return generateSignParams(fileId, null);
    }
    
    /**
     * 验证签名参数（预览场景使用，允许nonce重复）
     * 
     * @param fileId 文件ID
     * @param timestamp 时间戳
     * @param nonce 随机数
     * @param sign 签名
     * @return 验证结果，包含文件ID和是否有效
     */
    public TokenValidationResult validateSignParamsForPreview(Long fileId, Long timestamp, String nonce, String sign) {
        if (fileId == null || timestamp == null || nonce == null || sign == null) {
            logger.warn("参数不完整 - fileId: {}, timestamp: {}, nonce: {}, sign: {}", fileId, timestamp, nonce, sign);
            return new TokenValidationResult(false, null, "参数不完整");
        }
        
        // 对接收到的签名进行URL解码，处理+号被转换为空格的问题
        String decodedSign;
        try {
            decodedSign = java.net.URLDecoder.decode(sign, java.nio.charset.StandardCharsets.UTF_8);
        } catch (Exception e) {
            logger.warn("签名URL解码失败 - 文件ID: {}, sign: {}", fileId, sign);
            decodedSign = sign; // 如果解码失败，使用原始签名
        }
        
        // 检查是否过期
        if (System.currentTimeMillis() > timestamp) {
            logger.warn("参数已过期 - 文件ID: {}, 时间戳: {}", fileId, new java.util.Date(timestamp));
            return new TokenValidationResult(false, null, "参数已过期");
        }
        
        // 重新构造待签名字符串
        String signStr = fileId + timestamp + nonce + secretKey;
        
        // 双重MD5加密
        String expectedSign = doubleMd5(signStr);
        
        // 验证签名
        if (!decodedSign.equals(expectedSign)) {
            logger.warn("签名验证失败 - 文件ID: {}, nonce: {}, 期望签名: {}, 实际签名: {}", fileId, nonce, expectedSign, decodedSign);
            return new TokenValidationResult(false, null, "签名验证失败");
        }
        
        logger.info("参数验证成功 - 文件ID: {}, 时间戳: {}, nonce: {}", fileId, new java.util.Date(timestamp), nonce);
        return new TokenValidationResult(true, fileId, "验证成功");
    }
    
    /**
     * 验证签名参数（API调用场景使用，防止重放攻击）
     * 
     * @param fileId 文件ID
     * @param timestamp 时间戳
     * @param nonce 随机数
     * @param sign 签名
     * @return 验证结果，包含文件ID和是否有效
     */
    public TokenValidationResult validateSignParams(Long fileId, Long timestamp, String nonce, String sign) {
        if (fileId == null || timestamp == null || nonce == null || sign == null) {
            logger.warn("参数不完整 - fileId: {}, timestamp: {}, nonce: {}, sign: {}", fileId, timestamp, nonce, sign);
            return new TokenValidationResult(false, null, "参数不完整");
        }
        
        // 对接收到的签名进行URL解码，处理+号被转换为空格的问题
        String decodedSign;
        try {
            decodedSign = java.net.URLDecoder.decode(sign, java.nio.charset.StandardCharsets.UTF_8);
        } catch (Exception e) {
            logger.warn("签名URL解码失败 - 文件ID: {}, sign: {}", fileId, sign);
            decodedSign = sign; // 如果解码失败，使用原始签名
        }
        
        // 检查是否过期
        if (System.currentTimeMillis() > timestamp) {
            logger.warn("参数已过期 - 文件ID: {}, 时间戳: {}", fileId, new java.util.Date(timestamp));
            return new TokenValidationResult(false, null, "参数已过期");
        }
        
        // 检查nonce是否已被使用（防止重放攻击）
        if (usedNonces.contains(nonce)) {
            logger.warn("检测到重放攻击 - nonce已使用 - 文件ID: {}, nonce: {}", fileId, nonce);
            return new TokenValidationResult(false, null, "检测到重放攻击");
        }
        
        // 重新构造待签名字符串
        String signStr = fileId + timestamp + nonce + secretKey;
        
        // 双重MD5加密
        String expectedSign = doubleMd5(signStr);
        
        // 验证签名
        if (!decodedSign.equals(expectedSign)) {
            logger.warn("签名验证失败 - 文件ID: {}, nonce: {}, 期望签名: {}, 实际签名: {}", fileId, nonce, expectedSign, decodedSign);
            return new TokenValidationResult(false, null, "签名验证失败");
        }
        
        // 标记nonce为已使用（防止重放攻击）
        usedNonces.add(nonce);
        
        logger.info("参数验证成功 - 文件ID: {}, 时间戳: {}, nonce: {}", fileId, new java.util.Date(timestamp), nonce);
        return new TokenValidationResult(true, fileId, "验证成功");
    }

    /**
     * 验证版本化ID的签名参数（预览场景使用，允许nonce重复）
     * 
     * @param id 文件ID或版本ID
     * @param idType ID类型：file或version
     * @param timestamp 时间戳
     * @param nonce 随机数
     * @param sign 签名
     * @return 验证结果，包含原始ID和是否有效
     */
    public TokenValidationResult validateSignParamsForVersioned(Long id, String idType, Long timestamp, String nonce, String sign) {
        if (id == null || idType == null || timestamp == null || nonce == null || sign == null) {
            logger.warn("参数不完整 - id: {}, idType: {}, timestamp: {}, nonce: {}, sign: {}", 
                       id, idType, timestamp, nonce, sign);
            return new TokenValidationResult(false, null, "参数不完整");
        }
        
        if (!"file".equals(idType) && !"version".equals(idType)) {
            logger.warn("ID类型错误 - 原始ID: {}, ID类型: {}", id, idType);
            return new TokenValidationResult(false, null, "ID类型错误");
        }
        
        // 对接收到的签名进行URL解码，处理+号被转换为空格的问题
        String decodedSign;
        try {
            decodedSign = java.net.URLDecoder.decode(sign, java.nio.charset.StandardCharsets.UTF_8);
        } catch (Exception e) {
            logger.warn("签名URL解码失败 - 原始ID: {}, ID类型: {}, sign: {}", id, idType, sign);
            decodedSign = sign; // 如果解码失败，使用原始签名
        }
        
        // 检查是否过期
        if (System.currentTimeMillis() > timestamp) {
            logger.warn("参数已过期 - 原始ID: {}, ID类型: {}, 时间戳: {}", id, idType, new java.util.Date(timestamp));
            return new TokenValidationResult(false, null, "参数已过期");
        }
        
        // 重新构造待签名字符串
        String signStr = id + idType + timestamp + nonce + secretKey;
        
        // 双重MD5加密
        String expectedSign = doubleMd5(signStr);
        
        // 验证签名
        if (!decodedSign.equals(expectedSign)) {
            logger.warn("版本化ID签名验证失败 - 原始ID: {}, ID类型: {}, nonce: {}, 期望签名: {}, 实际签名: {}", 
                       id, idType, nonce, expectedSign, decodedSign);
            return new TokenValidationResult(false, null, "签名验证失败");
        }
        
        logger.info("版本化ID参数验证成功 - 原始ID: {}, ID类型: {}, 时间戳: {}, nonce: {}", 
                   id, idType, new java.util.Date(timestamp), nonce);
        return new TokenValidationResult(true, id, "验证成功");
    }
    
    /**
     * 双重MD5加密
     * 
     * @param str 待加密字符串
     * @return 双重MD5加密结果
     */
    public String doubleMd5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            
            // 第一次MD5
            byte[] firstDigest = md.digest(str.getBytes(StandardCharsets.UTF_8));
            String firstMd5 = Base64.getEncoder().encodeToString(firstDigest);
            
            // 第二次MD5
            byte[] secondDigest = md.digest(firstMd5.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(secondDigest);
        } catch (NoSuchAlgorithmException e) {
            logger.error("MD5算法不可用", e);
            throw new RuntimeException("MD5算法不可用", e);
        }
    }
    
    /**
     * 生成随机nonce
     * 
     * @return 随机nonce字符串
     */
    private String generateNonce() {
        // 使用当前时间戳和随机数生成唯一nonce
        long timestamp = System.currentTimeMillis();
        String randomPart = String.valueOf(Math.abs(java.util.UUID.randomUUID().toString().hashCode() % 1000000));
        return timestamp + "_" + randomPart;
    }
    
    /**
     * 清理过期的nonce
     */
    private void cleanupExpiredNonces() {
        // 由于ConcurrentHashMap不直接支持按时间清理，这里暂时不实现复杂清理
        // 实际应用中可以考虑使用更复杂的缓存机制如Caffeine
        logger.debug("执行nonce清理任务 - 当前nonce数量: {}", usedNonces.size());
    }
    
    /**
     * 签名参数类
     */
    public static class SignParams {
        public Long fileId;
        public Long timestamp;
        public String nonce;
        public String sign;
    }
    
    /**
     * 令牌验证结果类
     */
    public static class TokenValidationResult {
        private final boolean valid;
        private final Long fileId;
        private final String message;
        
        public TokenValidationResult(boolean valid, Long fileId, String message) {
            this.valid = valid;
            this.fileId = fileId;
            this.message = message;
        }
        
        public boolean isValid() {
            return valid;
        }
        
        public Long getFileId() {
            return fileId;
        }
        
        public String getMessage() {
            return message;
        }
    }
}